namespace Stee.CAP8.DBAccess
{
    using LinqKit;
    using Microsoft.EntityFrameworkCore;
    using Stee.CAP8.Entity;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Threading;
    using System.Threading.Tasks;

    public class Repository<T,TContext> : IRepository<T,TContext> where T : BaseEntity where TContext:DbContext
    {
        internal readonly UnitOfWork<TContext> UnitOfWork;
        internal readonly DbSet<T> DbSet;
        /// <summary>
        /// The constructor uses the unitofwork's context to create DbSet in the Repository
        /// </summary>
        public Repository(UnitOfWork<TContext> unitOfWork)
        {
            UnitOfWork = unitOfWork ?? throw new Exception();
            DbSet = UnitOfWork.Context.Set<T>();
        }
        /// <summary>
        /// Adds an entity into the database.
        /// <para>This method can be overridden by any class that inherits it.</para>
        /// </summary>
        virtual public void Add(T entity)
        {
            DbSet.Add(entity);
        }
        /// <summary>
        /// Adds an entity into the database.
        /// <para>Use SaveChangesAsync() to add an entity asynchronously</para>
        /// <para>This method is async only to allow special value generators, such as the one used by 'Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.SequenceHiLo', to access the database asynchronously. For all other cases the non async method should be used.</para>
        /// <para>This method can be overridden by any class that inherits it.</para>
        /// </summary>
        virtual public async Task AddAsync(T entity, CancellationToken cancellationToken = default)
        {
            await DbSet.AddAsync(entity, cancellationToken).ConfigureAwait(true);
        }
        /// <summary>
        /// Adds a variable number of entities to the database without the need to create a collection for them
        /// <para>This method can be overridden by any class that inherits it.</para>
        /// </summary>
        virtual public void AddRange(params T[] entities)
        {
            DbSet.AddRange(entities);
        }
        /// <summary>
        /// Adds a variable number of entities to the database without the need to create a collection for them.
        /// <para>Use SaveChangesAsync() to add the variable number of entities asynchronously</para>
        /// <para>This method is async only to allow special value generators, such as the one used by 'Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.SequenceHiLo', to access the database asynchronously. For all other cases the non async method should be used.</para>
        /// <para>This method can be overridden by any class that inherits it.</para>
        /// </summary>
        virtual public Task AddRangeAsync(CancellationToken cancellationToken = default, params T[] entities)
        {
            return DbSet.AddRangeAsync(entities, cancellationToken);
        }

        /// <summary>
        /// Adds the elements of the specified collection to the end of the List<T>.
        /// <para>This method can be overridden by any class that inherits it.</para>
        /// </summary>
        virtual public void AddRange(IEnumerable<T> entities)
        {
            DbSet.AddRange(entities);
        }
        /// <summary>
        /// Adds the elements of the specified collection to the end of the List<T>.
        /// <para>Use SaveChangesAsync() to add the collection of entities asynchronously</para>
        /// <para>This method is async only to allow special value generators, such as the one used by 'Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.SequenceHiLo', to access the database asynchronously. For all other cases the non async method should be used.</para>
        /// <para>This method can be overridden by any class that inherits it.</para>
        /// </summary>
        virtual public Task AddRangeAsync(IEnumerable<T> entities, CancellationToken cancellationToken = default)
        {
            return DbSet.AddRangeAsync(entities, cancellationToken);
        }

        /// <summary>
        /// This method can be overridden by any class that inherits it
        /// update an entity in the database
        /// Use SaveChangesAsync to update asynchronously
        /// </summary>
        virtual public void Update(T entity)
        {
            DbSet.Update(entity);
        }
        /// <summary>
        /// This method can be overridden by any class that inherits it
        /// update a variable number of entities to the database without the need to create a collection for them
        /// Use SaveChangesAsync to update asynchronously
        /// </summary>
        virtual public void UpdateRange(params T[] entities)
        {
            DbSet.UpdateRange(entities);
        }
        /// <summary>
        /// This method can be overridden by any class that inherits it
        /// update the elements of the specified collection to the end of the List<T>.
        /// Use SaveChangesAsync to update asynchronously
        /// </summary>
        virtual public void UpdateRange(IEnumerable<T> entities)
        {
            DbSet.UpdateRange(entities);
        }
        /// <summary>
        /// This method can be overridden by any class that inherits it
        /// Synchronously, retrieve a record by its with parameter id
        /// Set Property name with string propertyName
        /// Set Property value with object value
        /// </summary>
        virtual public void UpdateProperty(Guid id, string propertyName, object value)
        {
            var item = GetSingleOrDefault(x => x.ID == id);
            if (item != null)
            {
                item.GetType().GetProperty(propertyName).SetValue(item, value);
            }
            else
            {
                // log no item found
                throw new NullReferenceException();
            }
        }
        /// <summary>
        /// This method can be overridden by any class that inherits it
        /// Synchronously, delete an entity from the database
        /// </summary>
        virtual public void Delete(T entity)
        {
            if (entity != null)
            {
                entity.DataState = DataState.Deleted;
                Update(entity);
            }
        }
        /// <summary>
        /// This method can be overridden by any class that inherits it
        /// Synchronously, delete an entity from the database by finding its ID
        /// </summary>
        virtual public void DeleteByID(Guid id)
        {
            var item = GetSingleOrDefault(x => x.ID == id);
            if (item != null)
            {
                item.DataState = DataState.Deleted;
                Update(item);
            } else
            {
                // log no item found
                throw new NullReferenceException();
            }
        }
        /// <summary>
        /// This method can be overridden by any class that inherits it
        /// Synchronously, delete a variable number of entities from the database without the need to create a collection for them
        /// </summary>
        virtual public void DeleteRange(params T[] entities)
        {
            throw new NotImplementedException();
        }
        /// <summary>
        /// This method can be overridden by any class that inherits it
        /// Synchronously, delete the elements of the specified collection to the end of the List<T> from the database
        /// </summary>
        virtual public void DeleteRange(IEnumerable<T> entities)
        {
            throw new NotImplementedException();
        }
        /// <summary>
        /// Synchronously, query a record from the database with a condition LinQ expression.
        /// <para>If there are more than one record, EF core will throw exception. </para>
        /// <para>Example: GetSingle(x => x.ID.ToString() == "ABC");</para>
        /// </summary>
        public T GetSingle(Expression<Func<T, bool>> condition = null)
        {
            IQueryable<T> query = DbSet;

            if (condition != null)
            {
                query = query.Where(condition);
            }
            return query.Single();
        }
        /// <summary>
        /// Asynchronously, query a record from the database with a condition LinQ expression
        /// <para>If there are more than one record, EF core will throw exception. </para>
        /// <para>Example: GetSingleAsync(x => x.ID.ToString() == "ABC");</para>
        /// </summary>
        public Task<T> GetSingleAsync(
            Expression<Func<T, bool>> condition = null,
            CancellationToken cancellationToken = default)
        {
            IQueryable<T> query = DbSet;

            if (condition != null)
            {
                query = query.Where(condition);
            }

            return query.SingleAsync(cancellationToken);
        }
        /// <summary>
        /// Synchronously, query a record from the database with a condition LinQ expression.
        /// <para>If there is no record, it will return null. </para>
        /// <para>Example: GetSingleOrDefault(x => x.ID.ToString() == "ABC");</para>
        /// </summary>
        public T GetSingleOrDefault(Expression<Func<T, bool>> condition = null)
        {
            IQueryable<T> query = DbSet;

            if (condition != null)
            {
                query = query.Where(condition);
            }

            return query.SingleOrDefault();
        }
        /// <summary>
        /// Asynchronously, query a record from the database with a condition LinQ expression.
        /// <para>If there is no record, it will return null. </para>
        /// <para>Example: GetSingleOrDefaultAsync(x => x.ID.ToString() == "ABC");</para>
        /// </summary>
        public Task<T> GetSingleOrDefaultAsync(
            Expression<Func<T, bool>> condition = null,
            CancellationToken cancellationToken = default)
        {
            IQueryable<T> query = DbSet;

            if (condition != null)
            {
                query = query.Where(condition);
            }
            return query.SingleOrDefaultAsync(cancellationToken);
        }
        /// <summary>
        /// Synchronously, query a record from the database by using the Guid.
        /// <para>If there are more than one record, EF core will throw exception. </para>
        /// </summary>
        public T GetSingle(Guid id)
        {
            return DbSet.Where( x=> x.ID == id ).Single();
        }
        /// <summary>
        /// Asynchronously, query a record from the database by using the Guid.
        /// <para>If there are more than one record, EF core will throw exception. </para>
        /// </summary>
        public Task<T> GetSingleAsync(
            Guid id,
            CancellationToken cancellationToken = default)
        {
            return DbSet.Where(x => x.ID == id).SingleAsync(cancellationToken);
        }
        /// <summary>
        /// Synchronously, query a record from the database by using the Guid.
        /// <para>If there is no record, it will return null. </para>
        /// </summary>
        public T GetSingleOrDefault(Guid id)
        {
            return DbSet.Where(x => x.ID == id).SingleOrDefault();
        }
        /// <summary>
        /// Asynchronously, query a record from the database by using the Guid.
        /// <para>If there is no record, it will return null. </para>
        /// </summary>
        public Task<T> GetSingleOrDefaultAsync(
            Guid id,
            CancellationToken cancellationToken = default)
        {
            return DbSet.Where(x => x.ID == id).SingleOrDefaultAsync(cancellationToken);
        }
        /// <summary>
        /// Synchronously, query a list record from the database with a condition LinQ expression
        /// example: GetListAsync(x => x.State != "DELETED");
        /// </summary>
        public List<T> GetList(
            Expression<Func<T, bool>> condition = null,
            IList<OrderBy> orderBy = null,
            int pageIndex = 1,
            int pageSize = 10000)
        {
            var query = DbSet;
            var predicate = PredicateBuilder.New<T>(x => x.DataState == DataState.Active);

            if (condition != null)
            {
                predicate.And(condition);
            }
            if (orderBy != null)
            {
                if (orderBy.Count > 0)
                {
                    return query.Where(predicate).OrderBy(orderBy).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
                }
            }
            return query.Where(predicate).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
        }
        /// <summary>
        /// Asynchronously, query a list record from the database with a condition LinQ expression
        /// example: GetListAsync(x => x.State != "DELETED");
        /// </summary>
        public async Task<List<T>> GetListAsync(
            Expression<Func<T, bool>> condition = null,
            IList<OrderBy> orderBy = null,
            int pageIndex = 1,
            int pageSize = 10000,
            CancellationToken cancellationToken = default)
        {
            var query = DbSet;
            var predicate = PredicateBuilder.New<T>(x => x.DataState == DataState.Active);

            if (condition != null)
            {
                predicate.And(condition);
            }
            if (orderBy != null)
            {
                if (orderBy.Count > 0)
                {
                    return await query.Where(predicate).OrderBy(orderBy).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken).ConfigureAwait(true);
                }
            }
            return await query.Where(predicate).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken).ConfigureAwait(true);
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                UnitOfWork.Dispose();
            }
        }
    }
}
